#!/usr/bin/env ruby
require 'rubygems'
require 'faster_csv'

# Cluster data generator designed for the BIRCH System

# Dataset is generated with instances in K clusters.
# Instances are 2-d data points.
# Each cluster is characterized by the number of data points in it
# its radius and its center. The location of the cluster centers is
# determined by the pattern parameter. Three patterns are currently
# supported grid, sine and random.
# todo:

# (out of: BIRCH: An Efficient Data Clustering Method for Very Large
# Databases; T. Zhang, R. Ramkrishnan, M. Livny; 1996 ACM)
#
# This is mostly a translation/copy from the Weka project on sf.net written in
# java.

module Digger
  module Generator
    DEF_RAND_SPACE     = 100.0
    DEF_GRID_SPACE     = 10.0

    DEF_MIN_RADIUS     = 1.0
    DEF_MAX_RADIUS     = 3.0

    DEF_MIN_NUM_POINTS = 10
    DEF_MAX_NUM_POINTS = 30

    class ClusterFactory
      attr_reader :num_clusters, :num_dimensions, :pattern, 
        :min_radius, :max_radius, :min_num_points, :max_num_points

      def initialize(num_dimensions, args = {})
        @num_dimensions = num_dimensions
        @pattern = pattern
        @clusters = []

        puts "ARGS: " + args.inspect
        @rand_space = args[:rand_space] || DEF_RAND_SPACE
        @grid_space = args[:grid_space] || DEF_GRID_SPACE
        @min_radius = args[:min_radius] || DEF_MIN_RADIUS
        @max_radius = args[:max_radius] || DEF_MAX_RADIUS
        @min_num_points = args[:min_num_points] || DEF_MIN_NUM_POINTS
        @max_num_points = args[:max_num_points] || DEF_MAX_NUM_POINTS
      end

      def generate_clusters
        @num_clusters.times do
          @clusters << Cluster.new(self, next_center)
        end
      end

      def to_s
        str = ""
        @clusters.each {|c| str << c.to_s}
        str
      end

      def write(filepath)
        FasterCSV.open(filepath, "w") do |csv|
          @clusters.each {|c| c.points.each {|p| csv << p}}
        end
      end
    end

    class ClusterGrid < ClusterFactory
      def initialize(size, num_dimensions, args = {})
        super(num_dimensions, args)
        puts "Size: " + size.to_s + " Dims: " + num_dimensions.to_s
        @grid_size = size # size x size
        @num_clusters = size ** num_dimensions

        @grid_indices = Array.new(num_dimensions, 0)

        each_center(num_dimensions) {|center| @clusters << Cluster.new(self, center) }
      end

      def each_center(num_dimensions, base = [], &block)
        @grid_size.times do |i|
          center = base.dup
          center += [@grid_space * i]
          puts "#{num_dimensions} - #{i}: #{@grid_space * i}"

          if(num_dimensions > 1)
            each_center(num_dimensions - 1, center, &block)
          else
            puts "Center = " + center.inspect
            yield center
          end
        end
      end
    end

    class ClusterRandom < ClusterFactory
      def initialize(num_clusters, num_dimensions, args = {})
        @num_clusters = num_clusters
        super(num_dimensions, args)
      end

      def next_center
        center = []
        @num_dimensions.times do
          center << rand * @rand_space
        end

        puts "Center = " + center.join(' : ')
        center
      end
    end


    class Cluster
      attr_accessor :center, :radius, :points, :num_points

      def initialize(dataset, center)
        @dataset = dataset
        @center = center
        @radius = next_radius
        @num_points = next_num_points
        @points = generate_points
      end

      def generate_points
        points = []
        @num_points.times do |i| 
          point = []
          @dataset.num_dimensions.times {|i| 
            point << @center[i] + (rand - 0.5) * radius}
          points << point
        end

        points
      end

      def next_radius
        rand * (@dataset.max_radius - @dataset.min_radius) + @dataset.min_radius
      end

      def next_num_points
        (rand(@dataset.max_num_points - @dataset.min_num_points) + 
          @dataset.min_num_points).floor
      end

      def to_s
        str = ""
        @points.each {|p| p.each {|attr| str << "#{attr} "}; str << "\n"}
        str
      end
    end
  end
end

#Digger::Generator::ClusterFactory.new(8, 2).write('clusters.csv')
